#include "backend.h"
#include "filter.h"

#include <exception>
#include <stdexcept>
#include <climits>

/// Parameter:
/// Return:
/// Action: Frees the memory if necessery
///         Starts the filter in a seperate thread, to prevent blocking the UI.
///         When the result is ready, is is stored in the _modifiedImage member
void Backend::setup_result()
{
    if (_modifiedImage != 0)
        delete _modifiedImage;

    QFuture<int*> future = QtConcurrent::run(_filter, &Filter::use_filter);

    for (;;)
    {
        if (!future.isFinished())
            QCoreApplication::processEvents(QEventLoop::AllEvents);
        else
            break;
    }

    int* result = future.result();

    QCoreApplication::processEvents(QEventLoop::AllEvents);
    progress_to_gui(progress_max());

    _modifiedImage = new QImage((uchar*)result, _originalImage->width(),
                                    _originalImage->height(), _originalImage->format());
}

/// Parameter:
/// Return:
/// Action: Frees the memory if necessery
void Backend::setup_result_buffer()
{
    if (_filter->_modifiedData != 0)
    {
        delete[] _filter->_modifiedData;
        _filter->_modifiedData = 0;
    }
}

/// Parameter: v: progressbar's current value
/// Return:
/// Action: Forwards the progress signal from the Filter to the UI
void Backend::progress(int v)
{
    emit progress_to_gui(v);
}

/// Parameter:
/// Return:
/// Action: Constructor
///         Sets dynamic members to null pointer, and sets other data
Backend::Backend()
{
    _originalImage = 0;
    _modifiedImage = 0;
    _modifiedData = 0;
    _isParallel = false;
    _platformIsSet = false;
    _deviceIsSet = false;

    _filter = new Filter();
}

/// Parameter:
/// Return:
/// Action: Destructor
Backend::~Backend()
{
    delete _originalImage;
    delete _modifiedImage;
    delete _modifiedData;
    delete _filter;
}

/// Parameter: location: file location
/// Return:
/// Action: Loads an image from the given location parameter, and stores it in the _originalImage member.
///         Also deletes the modified image if present, and the corresponding data.
///         Note that the buffer must remain valid throughout the life of QImage.
///         Filter doesn't know, when to delete the buffer, so that is a public member, which we delete here.
void Backend::load_image(const QString location)
{
    _originalImageLocation = location;

    {
        QImage img(location);
        if (img.isNull())
        {
            std::runtime_error ex("Out of memory");
            throw ex;
        }

        if (img.width() * img.height() > INT_MAX)
        {
            std::runtime_error ex("Image is too large");
            throw ex;
        }
    }

    if (_originalImage != 0)
    {
        delete _originalImage;
        _originalImage = 0;
    }

    _originalImage = new QImage(location);
    if (_originalImage->isNull())
    {
        std::runtime_error ex("Out of memory");
        throw ex;
    }

    if (_modifiedImage != 0)
        delete _modifiedImage;
    _modifiedImage = 0;

    if (_modifiedData != 0)
        delete[] _modifiedData;
    _modifiedData = 0;
}

/// Parameter: location: file location
/// Return:
/// Action: Saves the modified image to the given location.
void Backend::save(const QString location)
{
    _modifiedImage->save(location);
}

/// Parameter: isParallel: true: use OpenCL, false: use sequential algorithm
/// Return:
/// Action: Sets whether the transformation should be in parallel or not.
void Backend::set_parallel(bool isParallel)
{
    if (isParallel)
    {
        if ((_platformIsSet) && (_deviceIsSet))
            _filter->set_parallel(isParallel, _platform, _device);
    }
    else
        _filter->disable_parallel();

    _isParallel = isParallel;
}

/// Parameter: filterMatrix: the filter matrix to be used in filter
///            matrixWidth: the filterMatrix's width
///            info: information about the transformation will be stored here
/// Return: modified image
/// Action: Blur transformation
///         Load the data to filter
///         Reconnect progressbar
///         Measure time
///         Set info
///         Get result
QImage *Backend::blur(float *filterMatrix, const int matrixWidth, TransformInfo &info)
{
    setup_result_buffer();

    int factor = matrixWidth * matrixWidth / 2 + 1;

    _filter->set_bias(0.0);
    _filter->set_factor(1.0 / factor);
    _filter->set_filter_matrix(filterMatrix, matrixWidth);

    _filter->set_original_data((int*)_originalImage->bits(), _originalImage->width(), _originalImage->height());

    disconnect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));
    connect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));

    QElapsedTimer timer;
    timer.start();
    setup_result();

    int time = timer.elapsed();
    emit finished(time);

    info._height = _modifiedImage->height();
    info._parallel = _isParallel;
    info._time = time;
    info._transformation = "blur";
    info._width = _modifiedImage->width();

    return _modifiedImage;
}

/// Parameter: filterMatrix: the filter matrix to be used in filter
///            info: information about the transformation will be stored here
/// Return: modified image
/// Action: Motion blur transformation
///         Load the data to filter
///         Reconnect progressbar
///         Measure time
///         Set info
///         Get result
QImage* Backend::motion_blur(float *filterMatrix, TransformInfo &info)
{
    setup_result_buffer();

    _filter->set_bias(0.0);
    _filter->set_factor(1.0 / 9.0);
    _filter->set_filter_matrix(filterMatrix, 9);
    _filter->set_original_data((int*)_originalImage->bits(), _originalImage->width(), _originalImage->height());

    disconnect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));
    connect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));

    QElapsedTimer timer;
    timer.start();
    setup_result();

    int time = timer.elapsed();
    emit finished(time);

    info._height = _modifiedImage->height();
    info._parallel = _isParallel;
    info._time = time;
    info._transformation = "motion blur";
    info._width = _modifiedImage->width();

    return _modifiedImage;
}

/// Parameter: info: information about the transformation will be stored here
/// Return: modified image
/// Action: Find horizontal edges transformation
///         Set filter matrix
///         Load the data to filter
///         Reconnect progressbar
///         Measure time
///         Set info
///         Get result
QImage* Backend::find_horizontal_edges(TransformInfo &info)
{
    float filterMatrix[25] =
    {
        0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,
       -1, -1,  2,  0,  0,
        0,  0,  0,  0,  0,
        0,  0,  0,  0,  0
    };

    _filter->set_bias(0.0);
    _filter->set_factor(1.0);
    _filter->set_filter_matrix(filterMatrix, 5);
    _filter->set_original_data((int*)_originalImage->bits(), _originalImage->width(), _originalImage->height());

    disconnect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));
    connect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));

    QElapsedTimer timer;
    timer.start();
    setup_result();

    int time = timer.elapsed();
    emit finished(time);

    info._height = _modifiedImage->height();
    info._parallel = _isParallel;
    info._time = time;
    info._transformation = "horizontal edges";
    info._width = _modifiedImage->width();

    return _modifiedImage;
}

/// Parameter: info: information about the transformation will be stored here
/// Return: modified image
/// Action: Find vertical edges transformation
///         Set filter matrix
///         Load the data to filter
///         Reconnect progressbar
///         Measure time
///         Set info
///         Get result
QImage* Backend::find_vertical_edges(TransformInfo &info)
{
    float filterMatrix[25] =
    {
        0,  0, -1,  0,  0,
        0,  0, -1,  0,  0,
        0,  0,  4,  0,  0,
        0,  0, -1,  0,  0,
        0,  0, -1,  0,  0
    };

    _filter->set_bias(0.0);
    _filter->set_factor(1.0);
    _filter->set_filter_matrix(filterMatrix, 5);
    _filter->set_original_data((int*)_originalImage->bits(), _originalImage->width(), _originalImage->height());

    disconnect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));
    connect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));

    QElapsedTimer timer;
    timer.start();
    setup_result();

    int time = timer.elapsed();
    emit finished(time);

    info._height = _modifiedImage->height();
    info._parallel = _isParallel;
    info._time = time;
    info._transformation = "vertical edges";
    info._width = _modifiedImage->width();

    return _modifiedImage;
}

/// Parameter: info: information about the transformation will be stored here
/// Return: modified image
/// Action: Find 45 degree edges transformation
///         Set filter matrix
///         Load the data to filter
///         Reconnect progressbar
///         Measure time
///         Set info
///         Get result
QImage* Backend::find_45_degree_edges(TransformInfo &info)
{
    float filterMatrix[25] =
    {
        -1,  0,  0,  0,  0,
         0, -2,  0,  0,  0,
         0,  0,  6,  0,  0,
         0,  0,  0, -2,  0,
         0,  0,  0,  0, -1
    };

    _filter->set_bias(0.0);
    _filter->set_factor(1.0);
    _filter->set_filter_matrix(filterMatrix, 5);
    _filter->set_original_data((int*)_originalImage->bits(), _originalImage->width(), _originalImage->height());

    disconnect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));
    connect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));

    QElapsedTimer timer;
    timer.start();
    setup_result();

    int time = timer.elapsed();
    emit finished(time);

    info._height = _modifiedImage->height();
    info._parallel = _isParallel;
    info._time = time;
    info._transformation = "45 degree edges";
    info._width = _modifiedImage->width();

    return _modifiedImage;
}

/// Parameter: info: information about the transformation will be stored here
/// Return: modified image
/// Action: Find all edges transformation
///         Set filter matrix
///         Load the data to filter
///         Reconnect progressbar
///         Measure time
///         Set info
///         Get result
QImage* Backend::find_all_edges(TransformInfo &info)
{
    float filterMatrix[9] =
    {
        -1, -1, -1,
        -1,  8, -1,
        -1, -1, -1
    };

    _filter->set_bias(0.0);
    _filter->set_factor(1.0);
    _filter->set_filter_matrix(filterMatrix, 3);
    _filter->set_original_data((int*)_originalImage->bits(), _originalImage->width(), _originalImage->height());

    disconnect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));
    connect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));

    QElapsedTimer timer;
    timer.start();
    setup_result();

    int time = timer.elapsed();
    emit finished(time);

    info._height = _modifiedImage->height();
    info._parallel = _isParallel;
    info._time = time;
    info._transformation = "all edges";
    info._width = _modifiedImage->width();

    return _modifiedImage;
}

/// Parameter: info: information about the transformation will be stored here
/// Return: modified image
/// Action: Sharpen transformation
///         Set filter matrix
///         Load the data to filter
///         Reconnect progressbar
///         Measure time
///         Set info
///         Get result
QImage* Backend::sharpen(TransformInfo &info)
{
    float filterMatrix[9] =
    {
        -1, -1, -1,
        -1,  9, -1,
        -1, -1, -1
    };

    _filter->set_bias(0.0);
    _filter->set_factor(1.0);
    _filter->set_filter_matrix(filterMatrix, 3);
    _filter->set_original_data((int*)_originalImage->bits(), _originalImage->width(), _originalImage->height());

    disconnect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));
    connect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));

    QElapsedTimer timer;
    timer.start();
    setup_result();

    int time = timer.elapsed();
    emit finished(time);

    info._height = _modifiedImage->height();
    info._parallel = _isParallel;
    info._time = time;
    info._transformation = "sharpen";
    info._width = _modifiedImage->width();

    return _modifiedImage;
}

/// Parameter: info: information about the transformation will be stored here
/// Return: modified image
/// Action: Find excessive edges transformation
///         Set filter matrix
///         Load the data to filter
///         Reconnect progressbar
///         Measure time
///         Set info
///         Get result
QImage* Backend::excessive_edges(TransformInfo &info)
{
    float filterMatrix[9] =
    {
        1,  1,  1,
        1, -7,  1,
        1,  1,  1
    };

    _filter->set_bias(0.0);
    _filter->set_factor(1.0);
    _filter->set_filter_matrix(filterMatrix, 3);
    _filter->set_original_data((int*)_originalImage->bits(), _originalImage->width(), _originalImage->height());

    disconnect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));
    connect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));

    QElapsedTimer timer;
    timer.start();
    setup_result();

    int time = timer.elapsed();
    emit finished(time);

    info._height = _modifiedImage->height();
    info._parallel = _isParallel;
    info._time = time;
    info._transformation = "excessive edges";
    info._width = _modifiedImage->width();

    return _modifiedImage;
}

/// Parameter: info: information about the transformation will be stored here
/// Return: modified image
/// Action: Emboss transformation
///         Set filter matrix
///         Load the data to filter
///         Reconnect progressbar
///         Measure time
///         Set info
///         Get result
QImage* Backend::emboss(TransformInfo &info)
{
    float filterMatrix[9] =
    {
        -1, -1,  0,
        -1,  0,  1,
         0,  1,  1
    };

    _filter->set_bias(128.0);
    _filter->set_factor(1.0);
    _filter->set_filter_matrix(filterMatrix, 3);
    _filter->set_original_data((int*)_originalImage->bits(), _originalImage->width(), _originalImage->height());

    disconnect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));
    connect(_filter, SIGNAL(progress(int)), this, SLOT(progress(int)));

    QElapsedTimer timer;
    timer.start();
    setup_result();

    int time = timer.elapsed();
    emit finished(time);

    info._height = _modifiedImage->height();
    info._parallel = _isParallel;
    info._time = time;
    info._transformation = "emboss";
    info._width = _modifiedImage->width();

    return _modifiedImage;
}

/// Parameter: Vector that will store the available OpenCL platforms
/// Return: Avalaible platforms in the parameter
/// Action: Gets the available platforms and stores them in the parameter
void Backend::avalaible_platforms(std::vector<cl::Platform>& platforms) const
{
    cl_int error = cl::Platform::get(&platforms);
    if (error != CL_SUCCESS)
    {
        std::runtime_error ex("Error while querying platforms");
        throw ex;
    }
}

/*void Backend::avalaible_devices(cl::Platform platform, std::vector<cl::Device> &devices) const
{
    cl_int error = platform.getDevices(CL_DEVICE_TYPE_ALL, &devices);
    if (error != CL_SUCCESS)
    {
        std::runtime_error ex("Devices");
        throw ex;
    }
}*/


/// Parameter: OpenCL platform
/// Return:
/// Action: Sets the OpenCL platform to be used
void Backend::set_platform(cl::Platform platform)
{
    _platform = platform;
    _platformIsSet = true;
}


/// Parameter: OpenCL device on the previously selected OpenCL platform
/// Return:
/// Action: Sets the device to be used
void Backend::set_device(cl::Device device)
{
    _device = device;
    _deviceIsSet = true;
}


/// Parameter:
/// Return:
/// Action: Modified image is used for further transformation
///         We copy the modified image to the original's place.
///         When QImage is out of memory, it return null image, if that happens, an exception is thrown.
void Backend::swap()
{
    if ((_modifiedImage == 0) || _modifiedImage->isNull())
    {
        _modifiedImage = 0;
        return;
    }

    *_originalImage = _modifiedImage->copy();

    if (_originalImage->isNull())
    {
        std::runtime_error ex("Error during changing image");
        throw ex;
    }
}
